Skip to content

fix(apply): refuse an unknown --build-id before tofu apply - #808

Merged
fas89 merged 3 commits into
mainfrom
claude/gallant-northcutt-bb55e2
Nov 4, 2026
Merged

fas89 merged 3 commits into
mainfrom
claude/gallant-northcutt-bb55e2

Conversation

@fas89

@fas89 fas89 commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Description

fluid apply <contract> --mode amend-and-build --build-id <id> (and replace-and-build) checked the id only in the build runner. The runner keeps the builds whose id equals --build-id and, with none left, logs Build not found and returns 1. On the OpenTofu engine (AWS, GCP, Snowflake, Confluent, Databricks and IaC plugin clouds) the runner starts after the apply hooks and tofu apply. So a mistyped --build-id provisioned the product first and then failed with a log line.

This PR runs the runner's own match at the start of the apply, right after the build-id grammar gate from #803. It runs in the build modes only, before the apply hooks, tofu and the runner, on both engines:

  • cli/apply.py::_gate_selected_build_for_apply refuses a --build-id that matches no builds[].id with CLIError apply_build_id_not_found (ERR_APPLY_BUILD_ID_NOT_FOUND). The error shows the id (as repr, so a stray space shows), the contract's build ids, a hint that ends "Nothing was applied", and catalog suggestions.
  • Ids match exactly, case and spaces included, as in the runner. An empty --build-id and a contract without builds are not checked, because the runner does not refuse them either. A build with no id matches nothing, as in the runner, and the hint says so.
  • The runner keeps its own filter as the backstop for callers that skip fluid apply.
  • The Command Center run now reports apply_build_id_not_found, where it reported build_not_found:<id> after the infrastructure was applied. tests/cli/test_apply_reports_to_command_center.py pins the new event.

Before and after, on the OpenTofu engine with --build-id no_such_build (the contract's build is make_rows):

before: tofu plan: +2 ~0 -0
        tofu apply complete: +2 ~0 -0
        ERROR Build not found: no_such_build                      exit 1

after:  ❌ apply_build_id_not_found  [ERR_APPLY_BUILD_ID_NOT_FOUND]
          build_id: 'no_such_build'
          available: ['make_rows']
          hint: --build-id selects the build whose id equals it exactly, case and spaces
                included, and no build of the contract has that id. Pass one of the
                available ids, or drop --build-id to run every build. Nothing was applied.
        exit 1, and tofu is never called

Not changed here, on purpose:

  • --build-id on a contract with no builds still exits 0, as the runner does.
  • No "maybe you meant" suggestion. Python's argparse has one for invalid choices (suggest_on_error, default on from 3.15), but none of the three other unknown-value errors (expose_not_found, rollback_snapshot_name_not_found, provider_unknown) has it. It belongs in one shared helper for all four.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would break existing functionality)
  • Documentation update
  • Refactor / code cleanup
  • New provider or provider enhancement

Documentation

  • Docs PR linked:
    • Docs PR: docs(apply): an unknown --build-id is refused before anything is applied (forge-cli#808) forge_docs#193. It is a draft, and is marked ready once this PR merges. It adds the following:
      • In docs/cli/apply.md: the --build-id match in the "Build ids" subsection, with the real output; a sentence on the --build-id flag row; and an errors-table row.
      • A bullet in docs/upgrading.md.
      • Rows in docs/advanced/production-troubleshooting.md, the page the CLI's docs link for this event opens.
      • A sentence in docs/advanced/operating-in-ci.md.
  • No docs needed
  • Docs TODO

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest)
  • I have run ruff check and black with no errors
  • New maintained Python files include license headers (python scripts/add_license_headers.py; examples/** exempt)

Testing

  • New tests: 32 cases in tests/cli/test_apply_build_ids_before_tofu.py, on fix(apply): refuse an unusable build id before tofu apply #803's harness: tofu and the apply hooks are recorders, and the real build runner runs behind a spy. On main 8755906, 21 of them fail and 11 pass (the 11 are controls), and the updated Command Center pin fails. All pass on the branch.
    • They cover both engines, the YAML and plan.json inputs, amend-and-build and replace-and-build, near misses (a typo, wrong case, a trailing space, a prefix), builds without an id, the order against the id-grammar gate, the CLI rendering through main(), and the runner backstop on the same near misses.
    • Controls: a known id reaches tofu and then the runner with that id, an empty --build-id filters nothing, and a contract without builds is not refused.
  • Suites for the touched modules, on this branch merged with main 26f1875: 869 passed, 1 skipped, across 23 files (apply, build runners, schedulers, CI templates, error catalog, license headers).
  • Live run of the real fluid CLI from a scratch directory, with a recording stand-in tofu first on PATH (shadowing a real OpenTofu 1.13.1) and no credentials. 27 of 27 checks pass:
    • GCP contract that fluid validate accepts, --build-id make_row: refused with ERR_APPLY_BUILD_ID_NOT_FOUND, 0 tofu calls and no .fluid/iac directory. The same holds with --dry-run, with replace-and-build --allow-data-loss, and for a plan.json written by the real fluid plan.
    • With --build-id make_rows the apply reaches tofu (control).
    • Native engine: an unknown id is refused and writes no output; a known id exits 0 and writes its parquet.
    • The same script on main 8755906: every unknown-id case on the OpenTofu engine reaches tofu and creates .fluid/iac, and the native one fails untyped.
  • Lint and policy gates: ruff (whole tree), the dead-code gate, black 24.10.0, license headers and the header-writer test, product guardrail and lint-imports (6 kept) all pass. detect-secrets 1.5.0 was run in a python:3.12-slim container on the changed files: rc 0, baseline unchanged. ruff --select F401,F841 on the changed files gives the same 17 hits as main.
  • Startup budget: tests/perf/test_startup_budget.py has the same module counts on main 26f1875 and on the branch (1031 and 1933). Its module-count failures in this venv predate this change.
  • Reviews of the diff: a defect scan (one logic and runtime hunter, one cross-file-contract and security hunter) and an independent security review found nothing. They checked that the gate and the runner see the same contract copy (YAML with --env, plan.json, .tgz bundle), the order against the plan-binding, data-loss, schema and id-grammar gates, every producer of --build-id (schedulers, CI templates, generate ci), ids made of escape sequences, Rich markup and secret shapes, and what the logs and Command Center record.

Found along the way, not in this PR: the Dagster and Prefect emitters in cli/_acquisition_stage_ext.py (lines 623 and 644) still generate the retired fluid apply --build <id>.

fas89 added 3 commits November 4, 2026 00:49
`fluid apply --mode amend-and-build --build-id <id>` (and replace-and-build)
checked the id only in the build runner, which keeps the builds whose id
equals it and, with none left, logs "Build not found" and returns 1. On the
OpenTofu engine the runner starts after `tofu apply`, so a mistyped id
provisioned the product and then failed with a log line and exit 1.

apply now runs the runner's match in a build mode, right after the build-id
grammar gate, ahead of the apply hooks, tofu and the runner, on both
engines. The refusal is a typed CLIError, apply_build_id_not_found, that
names the id, lists the contract's build ids and says nothing was applied.
Ids match exactly, as in the runner. An empty --build-id and a contract
without builds are not checked, as before. The runner keeps its own check
as the backstop.

The Command Center run now reports apply_build_id_not_found, where it
reported build_not_found:<id> after the infrastructure was applied.
@github-actions github-actions Bot added cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes labels Nov 4, 2026
@fas89
fas89 merged commit 635ea30 into main Nov 4, 2026
34 checks passed
@fas89
fas89 deleted the claude/gallant-northcutt-bb55e2 branch November 4, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant